前幾篇透過 Angular 管道元件大致說明了如何利用 Jasmine 來撰寫測試案例。然而除了管道之外,Angular 應用程式還有元件 (Component)、指令 (Direcitve) 與服務 (Service) 等各種元件類型,這一篇會說服在 Angular 服務中如何撰寫單元測試,來驗證程式的正確性。
這一篇會利用範例程式中的 OrderPricesComputeService
服務 (檔名為 services/order-price-compute.service.ts
)。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class OrderPriceComputeService {
constructor() {}
compute(values: number[]): number {
const total = values
.map((value) => value)
.reduce((acc, value) => acc + value, 0);
if (total > 5000) {
return total * 0.8;
} else if (total > 3000) {
return total * 0.9;
} else if (total > 1000) {
return total * 0.95;
} else {
return total;
}
}
}
在此此 Angular 服務中程式中,實作了當購物總金額在不同時可以有不同的折扣:
describe('TaiwanDatePipe', () => {
let pipe: TaiwanDatePipe;
beforeEach(() => {
pipe = new TaiwanDatePipe();
});
it('當日期為 2022/09/01 時,應回傳 "民國 111 年 9 月 1 日', () => {
...
});
it('當日期為 1900/09/01 時,應回傳 "民國前 11 年 9 月 1 日', () => {
...
});
});
如上面程式,在先前的範例中,我們利用 new TaiwanDatePipe()
建立初始化管道。這種初始化的方法也是可以使用在 Angular 服務的單元測試上。
describe('OrderPriceComputeService', () => {
let service: OrderPriceComputeService;
beforeEach(() => {
service = new OrderPriceComputeService();
});
it('購物總金額為 900 時,不享有任何折扣,付款金額應為 900', () => {
...
});
});
只是現實需求總是沒這麼單純,實際上可能在特定的 Angular 服務中相依於其他 Angular 服務。例如我們希望在 OrderPriceComputeService
執行 compute()
方法時,也呼叫 LogService
服務來記錄相關 Log 資訊(如下程式)。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class OrderPriceComputeService {
constructor(private logService: LogService) {}
compute(values: number[]): number {
const total = values
.map((value) => value)
.reduce((acc, value) => acc + value, 0);
this.logService.record(`折扣前購物金額為 ${total}`);
if (total > 5000) {
return total * 0.8;
} else if (total > 3000) {
return total * 0.9;
} else if (total > 1000) {
return total * 0.95;
} else {
return total;
}
}
}
如此一來,我們就必須在初始化 OrderPriceComputeService
服務時,也需要把 LogService
服務實體一併傳入建構式內。
describe('OrderPriceComputeService', () => {
let service: OrderPriceComputeService;
beforeEach(() => {
service = new OrderPriceComputeService(new LogService());
});
it('購物總金額為 900 時,不享有任何折扣,付款金額應為 900', () => {
...
});
});
然而,當在 Angular 服務的建構式是注入多個其他服務時,或是所注入的服務還有注入其他服務,都會大大增加測試時初始化服務的複雜度。
為了簡化測試時的初始化作業,Angular 提供了 TestBed 讓我們能在測試程式中配置與初始化的環境來模擬 @NgModule
,讓我們可以如同 Angular 應用程式一般,透過依賴注入 (Dependency Injection, DI) 的方式來管理元件之間的相依,進一步更容易地去測試依賴於 Angular 框架的行為。
因此,我們就可以修改上面測試程式內容,透過 TestBed 的 inject
方法來取得注入於 Angular 應用程式中的 OrderPriceComputeService
服務實體。
describe('OrderPriceComputeService', () => {
let service: OrderPriceComputeService;
beforeEach(() => {
service = TestBed.inject(OrderPriceComputeService);
});
});
在 Angular 8 之前,我們會使用 TestBed 的
get
方法取得服務實體,此方法已在 Angular 9 被棄用。
進一步,如果在 OrderPriceComputeService
服務有注入 LogService
服務時,我們可以利用 TestBed 的 configureTestingModule
方法來設定測試模組的服務提供者,其設定的方式與 @NgModel
相同。
describe('OrderPriceComputeService', () => {
let service: OrderPriceComputeService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [LogService]
});
service = TestBed.inject(OrderPriceComputeService);
});
});
如此一來就可以透過模擬 @NgModel
的方式,更容易的去測試 Angular 應用程式。
因此,我們可以利用這幾天所說明的 Jasmine 程式來完成此服務的所有測試情境,並執行 ng test
命令來確認驗證的結果。
這一篇利用 TestBed 來模擬 @NgModule
,以便於我們測試 Angular 應用程式,完整的測試程式放在 GitHub 中。接下來,就針對當 Angular 服務中使用 HttpClient 來存取後端服務時,其測試程式需要如何撰寫。